<!DOCTYPE doctype PUBLIC "-//w3c//dtd html 4.0 transitional//en">
<html><head>


  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <meta name="GENERATOR" content="A mano con el vim">
  <meta name="Author" content="Nicolas Wolovick">
  <meta name="Keywords" content="pthreads synchronization TAD sistemas operativos FaMAF education labs threadpool producer consumer">
  <style type="text/css">
	H1, H2, H3, DIV, P, LI,
	UL, OL, TABLE, TD, TR {font-family: lucida sans, helvetica, arial, sans-serif;}
	pre {background: #eee; border: 1px dashed #889; margin-left: 3em; padding: 1ex;}
	dt {font-family: monospace; font-weight: bold;}
  </style><title>Lab2: Implementación de un ThreadPool</title></head><body>

<!--============================================================-->
<h1><a class="autolink" title="Lab2: Implementación de un ThreadPool" href="http://educa.di.unc.edu.ar/mod/forum/view.php?id=3107">Lab2: Implementación de un ThreadPool</a></h1>
<h3> Natalia Bidart, Matías Cuenca-Acuña, Nicolás Wolovick</h3>

<!--============================================================-->
<h1>Objetivos</h1>
<ul>
	<li>Comprender y utilizar los mecanismos de concurrencia y sincronización de granularidad
		fina que provee POSIX (PThreads).
	</li>
	<li>Solucionar problemas típicos de concurrencia:
		condiciones de carrera, abrazos mortales, inanición, etc.
	</li>
	<li>Utilizar primitivas de sincronización estándar: secciones críticas, semáforos y variables de condición.
	</li>
	<li>Implementar TAD sincronizados para que funcionen en entornos concurrentes.
	</li>
</ul>


<!--============================================================-->
<h1>Motivación</h1>

<p>
Un ThreadPool es un <b>patrón de diseño</b> para la ejecución concurrente de tareas.
<br>
Usualmente, cuando tenemos algún problema que es paralelizable, descomponemos el problema en <b>unidades de ejecución</b> que serán ejecutadas por los <b>mecanismos de ejecución</b> del Sistema Operativo subyacente.
Típicamente estos mecanismos de ejecución son los procesos y los hilos.
<!--En este Laboratorio concentraremos nuestro trabajo en los hilos.-->

</p><p>
En muchos casos esta forma de dividir el problema no tiene una única respuesta en cuanto al <b>número de componentes</b> que participarán en la solución del problema.
<br>
Por ejemplo, supongamos que tenemos un procesador dual y queremos
computar la base de los logaritmos neperianos.
Podemos recurrir a la Serie de Taylor y dividir entre los términos de
la sumas que ocupan lugares pares e impares para alimentar cada CPU.
Cuando ambos hilos terminan, se computa la suma de ambas cantidades y
se retorna el resultado.
Si el hardware subyacente posee dos o más <b>unidades físicas de ejecución</b>, entonces nuestra descomposición aprovechará el harware y demorará la mitad del tiempo.

</p><pre width="75">1/0! +        1/2! +        1/4! + ...        + 1/n!     -&gt; Hilo0
       1/1! +        1/3! +        1/5! + ... + 1/(n+1)! -&gt; Hilo1
</pre>

<p>
Para otro tipo de trabajos como por ejemplo servir pedidos de múltiples clientes de <code><a href="http://es.wikipedia.org/wiki/Hypertext_Transfer_Protocol">HTTP</a></code>
(Firefox, elinks), podemos pensar que tenemos muchas unidades de
ejecución y simplemente atendemos cada pedido generando un nuevo hilo
de ejecución.
</p><pre width="90">"GET /robots.txt HTTP/1.1"						-&gt; hilo0
"GET /index.php?option=com_content&amp;task=view&amp;id=107&amp;Itemid=52 HTTP/1.1" -&gt; hilo1
"GET /dokuwiki/doku.php?id=introalg:int&amp;do=backlink HTTP/1.1"		-&gt; hilo2
"POST /wiki/doku.php HTTP/1.1"						-&gt; hilo3
.
.
.
"GET /so2004/Teoricos/clases.html HTTP/1.1"				-&gt; hiloN
</pre>

<p>
Notamos que el primer conjunto de tareas es <a href="http://en.wikipedia.org/wiki/Compute_bound">CPU-bound</a>, mientras que el segundo es claramente <a href="http://en.wikipedia.org/wiki/I/O_bound">I/O-bound</a>.

</p><p>Por otra parte, todos los cambios en el ciclo de vida de un
mecanismo de ejecución (creación, cambio de contexto, espera, muerte),
ya sean procesos o hilos, requiere de tiempo y en la mayoría de los
casos éste no es despreciable.
<br>Supongamos que tenemos un tiempo de creación y destrucción de
hilos de 1ms y la tarea a realizar toma también 1ms, entonces si por
cada pedido creamos y destruimos un hilo tendremos una sobrecarga del
50%.
</p><p>La idea es simple, generar una abstracción que cree una
cantidad fija de hilos, es decir un "pool de threads", que se
alimentará de una cola de tareas pendientes a realizar, que nuestro
programa alimentará con trabajos o <b>tasks</b>.
</p><p>
<img src="lab-threadpool_archivos/400px-Thread_pool.png" alt="ThreadPool">

</p><p>
Con este diseño logramos:
</p><ul>
	<li>Evitar todo el overhead y por consiguiente la <a href="http://en.wikipedia.org/wiki/Latency_%28engineering%29">latencia</a> de la creación y destrucción de hilos.</li>
	<li>Acotar el consumo de recursos (memoria, descriptores de archivos, tablas de procesos e hilos, etc.).</li>
	<li>Evitar problemas de sobrecarga y ataques por denegación de servicio - <a href="http://en.wikipedia.org/wiki/Denial_of_service">DOS attack</a>.</li>
	<li>Prevenir
que el Sistema Operativo alcance su límite para la cantidad de hilos
que maneja dado que la creación ilimitada puede llevar a perder
estabilidad.</li>
</ul>

<p>
Esta idea tiene también sus desventajas.
Por ejemplo, si mezclamos tareas CPU-bound con I/O-bound, las primeras
"taponarán" rápidamente el ThreadPool, generando que los tiempos de
respuesta para las segundas sean muy malos.
En general un ThreadPool resulta ideal para ejecutar tareas <b>independientes</b> y <b>homogéneas</b>.

</p><p>
Otro problema es decidir el tamaño ideal para el ThreadPool.
En [<a href="#biblio_1">1</a>] se recomienda:
</p><pre width="75">N_cpu = cantidad de unidades físicas de procesamiento
U_cpu = porcentaje de utilización de la CPU, 0≤U_cpu≤1
W/C = relación entre el tiempo de espera (W) y el tiempo de computación (C)

N_threads = N_cpu * U_cpu * (1 + W/C)
</pre>
<p>
Algunas implementaciones proponen un <b>tamaño dinámico</b> del ThreadPool a fin de ajustarlo a las necesidades del momento.
Estas implementaciones definen políticas de crecimiento y achicamiento del ThreadPool.

</p><p>
Sintéticamente podemos decir que <i>un ThreadPool implementa una forma asíncrona de ejecución de tareas que desacopla el envío de la ejecución de la tarea.
Además acota superiormente la cantidad de tareas que se pueden ejecutar de manera simultánea.
</i>

</p><h2>Implementaciones y usos</h2>

<p>
El proyecto Apache Jakarta implementa el componente <a href="http://commons.apache.org/sandbox/threadpool/">ThreadPool</a>, ofreciendo una solución de código abierto a la implementación del objeto <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/ThreadPoolExecutor.html">ThreadPoolExecutor</a> de la biblioteca de concurrencia <code>java.util.concurrent</code> de Java2 [<a href="http://educa.di.unc.edu.ar/file.php/142/Lab-ThreadPool/biblio_2">2</a>].

</p><p>
El servidor de http Apache en sus módulos MPM -Multi-Processing Module- implementa de manera experimental <a href="http://httpd.apache.org/docs/2.0/mod/threadpool.html"><code>threadpool</code></a> una variante del módulo estándard <a href="http://httpd.apache.org/docs/2.0/mod/worker.html"><code>worker</code></a>.


<!--============================================================-->
</p><h1>Tareas</h1>

<p>
Se deberá <b>completar</b> la implementación de un ThreadPool que se modulariza de la siguiente manera:

</p><p>
<table cellpadding="3" cellspacing="3">
<tbody><tr>
<td>
<img src="lab-threadpool_archivos/modulos.png" alt="diagrama de módulos">
</td>
<td>
&nbsp;&nbsp;&nbsp;&nbsp;
</td>
<td>
<p>
Por cada uno de los módulos tenemos:
</p><ul>
	<li><code>thread_pool</code>: se da la interface <a href="http://educa.di.unc.edu.ar/file.php/142/Lab-ThreadPool/thread_pool.h"><code>thread_pool.h</code></a> del módulo.</li>
	<li><code>task_queue</code>: se da la interface <a href="http://educa.di.unc.edu.ar/file.php/142/Lab-ThreadPool/task_queue.h"><code>task_queue.h</code></a> del módulo y una implementación cerrada <a href="http://educa.di.unc.edu.ar/file.php/142/Lab-ThreadPool/task_queue.o"><code>task_queue.o</code></a> (código objeto).</li>
	<li><code>task</code>: la implementación es abierta y completa: <a href="http://educa.di.unc.edu.ar/file.php/142/Lab-ThreadPool/task.h"><code>task.h</code></a>, <a href="http://educa.di.unc.edu.ar/file.php/142/Lab-ThreadPool/task.c"><code>task.c</code></a>.</li>
</ul>

<p>
Además se incluye un módulo de testeo del Threadpool, <a href="http://educa.di.unc.edu.ar/file.php/142/Lab-ThreadPool/test_thread_pool.c"><code>test_thread_pool.c</code></a> y el <a href="http://educa.di.unc.edu.ar/file.php/142/Lab-ThreadPool/Makefile"><code>Makefile</code></a> para poder compilar lo anterior.

</p></td>
</tr>
</tbody></table>

</p><p>
Dividimos la tarea de implementación en dos partes:
</p><ol>
	<li>Implementar <code>thread_pool</code> a partir de una implementación cerrada de <code>task_queue</code>.</li>
	<li>Implementar la cola sincronizada <code>task_queue</code>.</li>
</ol>

<!---------------------------------------------------------------->
<h2>1. Implementar ThreadPool</h2>

<p>
A partir del encabezado del TAD <code>thread_pool</code>, deberán implementarlo utilizando los módulos <code>{task_queue, task}</code>.

</p><pre width="75">#include "task.h" /* routine_t, arg_t */

typedef struct _thread_pool_t *thread_pool_t;

thread_pool_t thread_pool_create (const unsigned int pool_size,
                                  const unsigned int queue_size);
void thread_pool_execute_task (thread_pool_t p, routine_t f, arg_t arg);
thread_pool_t thread_pool_destroy (thread_pool_t p);

#endif /* __THREAD_POOL_H */
</pre>

<p>
El TAD <code>task_queue</code> es una <b>cola sincronizada</b> que almacena tareas.
<br>
Esta cola funciona correctamente en entornos multihilos, ya que está sincronizada apropiadamente.
Sus variables compartidas se acceden dentro de secciones críticas y las funciones <code>{task_queue_push,task_queue_pop}</code> están sincronizadas para que no se produzcan <i>buffers over/under runs</i>, es decir si un hilo llama a <code>push</code> con la cola llena, manda a dormir el hilo, y simétricamente para <code>pop</code> y la cola vacía.
<br>
Además del encabezado, se dá el código objeto <code>task_queue.o</code> listo para enlazarlo con la implementación.

</p><pre width="75">#ifndef __TASK_QUEUE_H
#define __TASK_QUEUE_H

#include "task.h" /* task_t */

typedef struct _task_queue_t *task_queue_t;

task_queue_t task_queue_create (const unsigned int size);
task_queue_t task_queue_push (task_queue_t q, task_t t);
task_queue_t task_queue_pop (task_queue_t q, task_t *t);
task_queue_t task_queue_destroy (task_queue_t q);
void task_queue_print (task_queue_t q);
unsigned int task_queue_count (task_queue_t q);

#endif /* __TASK_QUEUE_H */
</pre>

<p>
El TAD que encapsula la tarea es básicamente un par <i>(rutina, argumento)</i> que hace uso de la posibilidad que tiene "C" de <b>pasar parámetros y devolver resultados de tipo función</b>.

<table>
<tbody><tr>
<td>
<pre>#ifndef __TASK_H
#define __TASK_H

typedef void *arg_t;

typedef void *(*routine_t)(void *);

typedef struct _task_t *task_t;

task_t task_create (routine_t f, arg_t arg);
routine_t task_routine (task_t t);
arg_t task_arg (task_t t);
task_t task_destroy (task_t t);

#endif /* __TASK_H */
</pre>
</td>
<td>
<pre>#include &lt;stdlib.h&gt; /* calloc */
#include &lt;assert.h&gt; /* assert */
#include "task.h" /* routine_t, arg_t, task_t */

struct _task_t {
	routine_t f;
	arg_t arg;
};

task_t task_create (routine_t f, arg_t arg) {
	task_t result = NULL;
	
	/* PRE: */assert (f != NULL);
	
	result = calloc (1, sizeof (*result));
	result-&gt;f = f;
	result-&gt;arg = arg;
	
	/* POST: */assert (result != NULL);
	/* POST: */assert (result-&gt;f == f);
	/* POST: */assert (result-&gt;arg == arg);
	return (result);
}

routine_t task_routine (task_t t) {
	/* PRE: */assert (t != NULL);
	return (t-&gt;f);
}

arg_t task_arg (task_t t) {
	/* PRE: */assert (t != NULL);
	return (t-&gt;arg);
}

task_t task_destroy (task_t t) {
	/* PRE: */assert (t != NULL);
	
	free (t);
	t = NULL;
	/* POST: */assert (t == NULL);
	return (t);
}
</pre>
</td>
</tr>
</tbody></table>

</p><p>
Para probar esta y la siguiente implementación proponemos la generación
de tareas CPU-bound que puede resultar útil, el cómputo de la
primalidad de un número.
<br>
La idea es tomar un número <i>n</i> y dividir el intervalo de búsqueda de divisores <i>[2,√n]</i> en <i>k</i> partes iguales y cada una de esas partes será la tarea <i>i</i>-ésima: "buscar en el intervalo <i>[l,u]</i> si <i>n</i> tiene algún divisor".
<br>
Notar que si disponemos de <b>más de una unidad física de ejecución</b>,
entonces el tiempo de cómputo se dividirá por la cantidad de unidades
disponibles (la mitad en computadoras con microprocesadores duales).
</p><p>
<table>
<tbody><tr>
<td>
<pre>#include &lt;stdio.h&gt; /* printf */
#include &lt;stdbool.h&gt; /* bool, true, false */
#include &lt;assert.h&gt; /* assert */
#include &lt;stdlib.h&gt; /* calloc, free */
#include &lt;math.h&gt; /* ceil, sqrt, fmod */
#include &lt;stdlib.h&gt; /* strtoull */
#include &lt;pthread.h&gt; /* pthread_mutex_{t, init, destroy} */
                     /* pthread_cond_{t, init, destroy, wait, signal} */
#include "thread_pool.h" /* thread_pool_{t, create, destroy, execute_task} */

#define BOTTOM 2
#define POOL_SIZE 1
#define QUEUE_SIZE 100
#define CHUNK_SIZE 1000

/* shared data */
unsigned long long int number_to_test = 0, intervals = 0, tested = 0;
bool is_prime = true;
pthread_mutex_t mutex;
pthread_cond_t done;

/* testing routine's prototype */
void *check_primality (void *arg);

/* helper struct to pass useful arguments to testing routine */
struct range {
	unsigned long long int start;
	unsigned long long int end;
};

/* main routine */
int main (int argc, char *argv[]) {
	unsigned int i = 0;
	double q = 0.0;
	struct range *tests = NULL;
	thread_pool_t p = NULL;
	
	/* grab number to test from arguments */
	if (argc &lt; 2) {
		printf ("Usage: test_thread_pool <number to="" test="" for="" primality="">.\n");
		return (-1);
	}
	assert (argc &gt; 1);
	number_to_test = strtoull (argv[1], NULL, 10);
	
	/* from: http://en.wikipedia.org/wiki/Primality_testing
	 *
	 * Rather than testing all numbers up to `number_to_test - 1', we need
	 * only test numbers up to `sqrt (number_to_test)': if `number_to_test'
	 * is composite then it can be factored into two values, at least one
	 * of which must be less than or equal to `sqrt (number_to_test)'.
	 */
	q = sqrt (number_to_test);
	
	/* divide work for threads */
	intervals = ceil (q / CHUNK_SIZE);
	
</number></pre>
</td>

<td>
<pre>	/* init stuff */
	pthread_mutex_init (&amp;mutex, NULL);
	pthread_cond_init (&amp;done, NULL);
	
	tests = calloc (intervals, sizeof (struct range));
	assert (tests != NULL);
	
	p = thread_pool_create (POOL_SIZE, QUEUE_SIZE);
	assert (p != NULL);
	
	/* execute tasks through thread pool `p' */
	for (i = 0; i &lt; intervals &amp;&amp; is_prime; i++) {
		tests[i].start = BOTTOM + i * CHUNK_SIZE;
		if (i == intervals - 1) { /* last interval */
			tests[i].end = ceil (q) + 1; /* the nearest integer to `q' */
		} else {
			tests[i].end = tests [i].start + CHUNK_SIZE;
		}
		thread_pool_execute_task (p, check_primality, (void *)&amp;tests[i]);
	}
	
	/* wait for all intervals to be processed */
	pthread_cond_wait (&amp;done, &amp;mutex);
	
	/* print result on stdout */
	printf ("test_thread_pool.c: main: %lli is %sprime!\n",
	        number_to_test, (is_prime ? "" : "not "));
	
	/* destroy stuff */
	pthread_mutex_destroy (&amp;mutex);
	pthread_cond_destroy (&amp;done);
	p = thread_pool_destroy (p);
	free (tests);
	tests = NULL;
	
	return (0);
}

/* testing routine */
void *check_primality (void *arg) {
	struct range interval;
	unsigned int i = 0;
	
	interval = *(struct range *)arg;
	for (i = interval.start; i &lt; interval.end &amp;&amp; is_prime; i++){
		if (i != number_to_test &amp;&amp; fmod (number_to_test, i) == 0)
			is_prime = false;
	}
	assert (i &gt;= interval.end || !is_prime);
	tested++;
	
	if (!is_prime || tested == intervals)
		pthread_cond_signal (&amp;done);
	
	return NULL;
}
</pre>
</td>

</tr>
</tbody></table>


<!---------------------------------------------------------------->
</p><h2>2. Implementar la cola FIFO sincronizada</h2>

<p>
A partir de la interface <code>task_queue.h</code> deberán armar una implementación de una cola FIFO sincronizada de tareas para entornos multihilos.
<br>
Se pide:
</p><ul>
	<li>Sincronizar los accesos concurrentes a variables compartidas.</li>
	<li>Sincronizar los pedidos de <code>{push,pop}</code> para que nunca se complete un <code>pop</code> con la cola vacía, ni un <code>push</code> con la cola llena.</li>
</ul>

<p>
Más específicamente:
</p><ul>
	<li>Proponer una solución a estos problemas utilizando las primitivas de sincronización estudiadas.</li>
	<li>Implementar las soluciones sincronizando <b>internamente</b> el TAD (es decir el <tt>main()</tt> no tiene que cambiar).</li>
	<li>Comprobar de manera exhaustiva que no hay problemas de concurrencia.</li>
	<li>Analizar el <b>grado de paralelismo</b>, tratando que este sea el mayor posible sin comprometer la consistencia de los TADs.</li>
</ul>



<p>Se pueden utilizar cualquiera de los constructores de sincronización
de PThreads (mutex, semáforos, variables de condición, etc.).
</p><p>
Deberán incluir <code>asserts</code> en lugares significativos del código para que ante la primer <a href="http://en.wikipedia.org/wiki/Race_condition">race condition</a> se aborte la ejecución del programa, <b>mostrando de manera clara que hay problemas de sincronización</b>.

</p><p>
Deberán <b>testear de manera exhaustiva</b> la implementación se pueden usar computadoras SMP con dos procesadores o procesadores duales, o bien utilizar <code>sched_yield()</code> de forma aleatoria como se muestra en "<a href="">PThreads, un tutorial</a>".


<!--............................................................-->
</p><h3>Algunos consejos para derrotar el no-determinismo del scheduler</h3>

<p>
Resulta fundamental <b>no confiar jamás en un par de ejecuciones
exitosas</b>, probablemente con otra forma de <tt>sched_yield()</tt> nuestro
multiprograma ya no funcione correctamente.
Recuerden que <b>el planificador es nuestro peor enemigo</b>
y cosas que aquí y ahora funcionan correctísimamente,
en dos días y en otra máquina pueden fallar siempre.
Prueben sus códigos terminados en diferentes máquinas,
con distintas versiones del SO y bajo distintas condiciones de carga.

</p><p>
Como hacer debugging en programas multihilos es casi magia negra, resulta
absolutamente indispensable utilizar <b>programación defensiva</b>
con <tt>asserts</tt> y manejo de errores de <tt>syscalls</tt>, además
de estar más que seguros que los TAD funcionan correctamente en un
entorno monoprogramación.

</p><p>
Utilicen todos los artilugios para <b>generar interleavings ricos</b>
(<tt>srand</tt>, <tt>/dev/urandom</tt>, etc.), a fin de asegurar que
cada ejecución producirá un interleaving distinto
con alta probabilidad.

</p><p>
<b>Imprimir información de debugging</b> resulta también
fundamental.
Es posible generar un esquema de debugging con salida controlada,
definiendo  una función <tt>void debug(char *mensaje)</tt>, que
internamente controle  si los mensajes se eliminarán o se
escribirán y a donde.
Probablemente una función
<tt>void debug(unsigned int nivel, char *mensaje)</tt>,
sea un poco más sofisticada, pero también
más útil, y básicamente imprime todos los mensajes
que tienen un nivel de debugging por debajo de una constante <tt>DEBUG</tt>,
con lo cual los mensajes con nivel bajo son generales y a medida que subimos
de nivel aumenta la especificidad.
<br>
Como la escritura de la información de debugging es manejada
por la <tt>libc</tt>, y está suele hacerlo de manera asíncrona
(puede escribir un poco después de que se llamó la función),
es <b>posible forzar la sincronicidad</b>, por ejemplo con la llamada de la
<tt>libc</tt> <tt>fflush(stdout)</tt>, a fin de obtener un temporizado
más preciso de los mensajes.

</p><p>
Una buena <b>función de impresión del TAD</b>, también
puede ayudar mucho, así como <b>funciones de autotesteo</b> del estilo
<tt>table_autotest</tt> que comprueben
cosas tan básicas como que pedir un índice de la tabla y después
preguntar si está disponible, sea falso.



</p><h1>Cómo atacar los problemas</h1>

<p>Se recomienda estudiar todo el código dado en papel, y pensar las
posibles implementaciones, sobre todo en la sincronización de la
primera y segunda tarea.
También se recomienda la lectura de "<a class="autolink" title="PThreads, un tutorial" href="http://educa.di.unc.edu.ar/mod/resource/view.php?id=3110">PThreads, un tutorial</a>", probando los <a class="autolink" title="Ejemplos" href="http://educa.di.unc.edu.ar/mod/resource/view.php?id=3117">ejemplos</a> y resolviendo algunos ejercicios.
<br>
También es importante, <b>relacionar</b> los problemas de sincronización con los
que se dieron en el teórico (sección crítica,
productor-consumidor, lectores-escritores, filósofos comensales, etc.),
pues aunque los problemas pueden no ser exactamente iguales, la solución puede surgir de una combinación de ellos.


</p><h1>Qué se debe Entregar</h1>

<p>
Todos los códigos que les damos <b>no deberán ser modificados</b>, con lo cual solo se agregarán las implementaciones <code>{thread_pool.c, task_queue.c}</code>.

</p><p>
En la documentación se deberá decir el como y el porqué de cada una de las sincronizaciones incluidas en estos códigos.


</p><p>
</p><h1>Tareas Adicionales</h1>

<p>
Si les sobra tiempo pueden hacer las siguientes mejoras:
</p><ul>
	<li>ThreadPool con tamaño dinámico, dentro de un mínimo y un
máximo. Implementar al menos un par de políticas de crecimiento y
decrecimiento.</li>
</ul>



<!-- ====================================================================  -->
<h1>Bibliografía</h1>
<ul>
	<li><a name="biblio_1"></a>Goetz, Biran, "<a href="http://www.javaconcurrencyinpractice.com/">Java Concurrency in Practice</a>", Pearson Education, 2006.
	</li>
	<li><a name="biblio_2"></a>Lea, Doug, "<a href="http://www.javaconcurrencyinpractice.com/">Concurrent Programming in Java: Design Principles and Patterns</a>", second edition, Addison-Wesley, 1999.
	</li>
</ul>


<!-- Footer -->
<div align="right"> <br>
	<font size="-1">
	$Date: 2007-09-12 16:25:36 -0300 (Wed, 12 Sep 2007) $,
	$Revision: 36 $
	</font>
</div>

</body></html>